ClickHouse

20.7.3.14

20.5 final

20.6.3 explain

1. 介绍

  1. 俄罗斯Yandex开发。
  2. 列式存储数据库(相较于MySQL等行式存储数据库区别)。
    • 列式存储是指数据在磁盘中以列的方式进行存储。相较之下查询相同列的数据的时候比较快,查询行的数据比较慢,可能需要遍历整张表,但是修改的代价高。列式存储对聚合、计数、求和等操作速度远快于行式存储,更适合进行查询分析的场景。某一列的数据类型是相同的,可以进行数据压缩。
    • 行式存储查询行的数据比较快,但是查询某一列的数据就比较慢,可能要遍历整张表,但是修改的代价低。更适合进行联机修改的场景。
    • 类比成数据结构中的线性表的优缺点。
  3. 用途:在线分析处理查询(OLAP),能够使用SQL查询实时生成分析数据报告。
  4. C++编写。
  5. 可以使用SQL语句进行操作。
  6. 引擎分类:合并树(Merge Tree)、日志、接口、其他四大类20多种。
  7. 高吞吐写入能力。采用LSM Tree结构,数据写入是顺序append写,写入后不可更改。数据写入定期后台Compaction,多个段merge sort后顺序写回磁盘。
  8. 数据分区和线程级并行,单条Query就能利用整机所有CPU,吃CPU,对于高qps的查询业务不是强项。
  9. 适合大量数据的宽表场景。
  10. 避免join操作(把右表加载到内存,再和左表匹配)。

2. 安装

  1. 关闭防火墙或者开放端口。

  2. 取消Linux上的文件数及用户最多进程数的限制。

    $ ulimit -a
    
    主要修改的是这两个
    open files
    max user processes
    
    $ sudo vim /etc/security/limits.conf
    * soft nofile 65536
    * hard nofile 65536
    * soft nproc 131072
    * hard nproc 131072
    
    * 限定域为所有用户
    soft软限制,日常状态/hard硬限制,最大上限
    
    $ sudo vim /etc/security/limits.d/20-nproc.conf
    * soft nofile 65536
    * hard nofile 65536
    * soft nproc 131072
    * hard nproc 131072
    
  3. 重新登录。

  4. 安装依赖。

    sudo yum install -y libtool
    sudo yum install -y *unixODBC*
    
  5. CentOS取消SELINUX

    SE:security enforce.

    $ sudo vim /etc/selinux/config
    SELINUX=disabled
    
  6. 重启。

  7. 安装。

    sudo yum install clickhouse-server clickhouse-client
    
    vim /etc/clickhouse-server/config.xml
    # 把 <listen_host>::</listen_host> 的注释打开,让CH支持远程访问。
    
  8. 重启服务。

3. 数据类型

3.1. 整型

固定长度的整型,包括有符号整型或无符号整型。 整型范围(-2n-1~2n-1-1):
Int8 - [-128 : 127]
Int16 - [-32768 : 32767]
Int32 - [-2147483648 : 2147483647]
Int64 - [-9223372036854775808 : 9223372036854775807]

无符号整型范围(0~2n-1):
UInt8 - [0 : 255]
UInt16 - [0 : 65535]
UInt32 - [0 : 4294967295]
UInt64 - [0 : 18446744073709551615]
使用场景: 个数、数量、也可以存储型 id。


Int8 - byte
Int16 - short
Int32 - int
Int64 - long

3.2. 浮点型

Float32 - float
Float64 – double

浮点型进行计算时可能引起四舍五入的误差。

使用场景:一般数据值比较小,不涉及大量的统计计算,精度要求不高的时候。比如 保存商品的重量。

3.3. 布尔型

没有单独的类型来存储布尔值。可以使用 UInt8 类型,取值限制为 0 或 1。

3.4. Decimal 型

有符号的浮点数,可在加、减和乘法运算过程中保持精度。对于除法,最低有效数字会 被丢弃(不舍入)。

有三种声明:

Decimal32(s),相当于Decimal(9-s,s),有效位数为1~9
Decimal64(s),相当于Decimal(18-s,s),有效位数为1~18
Decimal128(s),相当于Decimal(38-s,s),有效位数为1~38

s 标识小数位 使用场景:一般金额字段、汇率、利率等字段为了保证小数点精度,都使用 Decimal进行存储。

3.5. 字符串

String

字符串可以任意长度的。它可以包含任意的字节集,包含空字节。

FixedString(N)

固定长度 N 的字符串,N 必须是严格的正自然数。当服务端读取长度小于 N 的字符

串时候,通过在字符串末尾添加空字节来达到 N 字节长度。 当服务端读取长度大于 N 的 字符串时候,将返回错误消息

使用场景:称、文字描述、字符型编码。 固定长度的可以保存一些定长的内容,比 如一些编码,性别等但是考虑到一定的变化风险,带来收益不够明显,所以定长字符串使用 意义有限。

3.6. 枚举类型

包括 Enum8 和 Enum16 类型。Enum 保存 'string'= integer 的对应关系。

Enum8 用 'String'= Int8 对描述。
Enum16 用 'String'= Int16 对描述。
CREATE TABLE t_enum
(
   x Enum8('hello' = 1, 'world' = 2)
)
ENGINE = TinyLog;

-- 此时存储可以使用key存储,也可以使用value进行存储。select时,默认显示key,可以使用Cast(colName, toTypeName(value))显示value值。

使用场景:对一些状态、类型的字段算是一种空间优化,也算是一种数据约束。但是实际使用中往往因为一些数据内容的变化增加一定的维护成本,甚至是数据丢失问题。所以谨慎使用。

3.7. 时间类型

目前 ClickHouse 有三种时间类型

Date接受年-月-日的字符串比如‘2019-12-16’
Datetime 接受年-月-日 时:分:秒的字符串比如 ‘2019-12-16 20:50:10’
Datetime64 接受年-月-日 时:分:秒.亚秒的字符串比如‘2019-12-16 20:50:10.66’

日期类型,用两个字节存储,表示从 1970-01-01 (无符号) 到当前的日期值。

3.8. 数组

Array(T):由 T 类型元素组成的数组。

T 可以是任意类型,包含数组类型。 但不推荐使用多维数组,ClickHouse 对多维数组的支持有限。例如,不能在 MergeTree 表中存储多维数组。

创建数组的方式:
Array(1,2)
[1,2]

4. 表引擎

4.1. TinyLog

不支持索引,没有并发控制。一般保存少量数据的小表, 生产环境上作用有限。

create table t_tinylog (id String, name String) engine=TinyLog;

4.2. Memory

内存引擎,数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失。读写操作不会相互阻塞,不支持索引。

4.3. MergeTree

支持索引和分区。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
    INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
ORDER BY expr
[PARTITION BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTINGS name=value, ...]

PARTITION BY分区

降低扫描的范围,优化查询速度。

可选项,如果不选默认使用一个all分区。

分区后,面对涉及跨分区的查询统计,ClickHouse 会以分区为单位并行处理。

数据写入与分区合并:任何一个批次的数据写入都会产生一个临时分区,不会纳入任何一个已有的分区。写入后的某个时刻(大概 10-15 分钟后),ClickHouse 会自动执行合并操作(等不及也可以手动通过 optimize 执行),把临时分区的数据,合并到已有分区中。

OPTIMIZE TABLE table_name PARTITION partition_name FINAL;

透过自带的client查询的时候,数据会显示分区的详情,透过其他工具则不会有。

数据在分区内有序。

PRIMARY KEY主键

ClickHouse中的主键只有一级索引,并没有唯一性约束

在where语句中根据主键查询信息,会对主键进行某种形式的二分查找,从而定位到index granularity信息,避免全盘扫描。

index granularity:索引粒度,指在稀疏索引中两个相邻索引对应数据的间隔。ClickHouse 中的 MergeTree 默认是 8192。

要求:主键必须是 order by 字段的前缀字段。

ORDER BY 排序字段【必选】

此字段设定了分区内的数据按照哪些字段顺序进行有序保存,是 MergeTree 中唯一一个必填项。

二级索引

INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1

GRANULARITY value1 是设定二级索引对于一级索引粒度的粒度。

create table t_order_mt2(
   id UInt32,
sku_id String,
total_amount Decimal(16,2), create_time Datetime,
    INDEX a total_amount TYPE minmax GRANULARITY 5
 ) engine =MergeTree
  partition by toYYYYMMDD(create_time)
  primary key (id)
    order by (id, sku_id);

TTL

TTL 即 Time To Live,MergeTree 提供了可以管理数据表或者列的生命周期的功能。

列级别TTL
name1 [type1] [TTL expr1],
create table t_order_mt3(
   id UInt32,
   sku_id String,
   total_amount Decimal(16,2) TTL create_time+interval 10 SECOND, 
   create_time Datetime
 ) engine =MergeTree
 partition by toYYYYMMDD(create_time)
 primary key (id)
 order by (id, sku_id);

 -- total_amount Decimal(16,2) TTL create_time+interval 10 SECOND, 
 -- 在create_time时间的10秒之后total_amount字段变成默认值

需要合并后才会看到效果。

表级别TTL
alter table table_name MODIFY TTL create_time + INTERVAL 10 SECOND;
-- 表数据会在 create_time 之后 10 秒消失。

涉及判断的字段必须是 Date 或者 Datetime 类型,推荐使用分区的日期字段。

4.4. ReplacingMergeTree

存储特性完全继承 MergeTree,只是 多了一个去重的功能。

会根据order by字段去重,并不是根据primary key字段去重。

去重的过程只会在合并的过程中出现,合并会在后台不定时进行,无法进行预知,所以有些数据可能还未被处理。

如果表经过了分区,则只会在分区内进行去重。

适用于在后台清理重复的数据以节省空间,不能保证没有重复的数据出现。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = ReplacingMergeTree([ver])
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

ENGINE = ReplacingMergeTree([ver])填入的为版本字段,重复的数据保留版本值最大的。

如果不填ver或者ver相同,则默认按照插入顺序保留最后一条。

4.5. SummingMergeTree

对于不查询明细,只关心以维度进行汇总聚合结果的场景。如果只使用普通的 MergeTree 的话,无论是存储空间的开销,还是查询时临时聚合的开销都比较大。

ClickHouse 为了这种场景,提供了一种能够“预聚合”的引擎 SummingMergeTree。

create table t_order_smt(
   id UInt32,
   sku_id String,
   total_amount Decimal(16,2) ,
   create_time Datetime
) engine =SummingMergeTree(total_amount)
  partition by toYYYYMMDD(create_time)
  primary key (id)
  order by (id,sku_id );

engine =SummingMergeTree(total_amount)中,total_amount列作为汇总数据列。

可以填写多列必须数字列,如果不填,以所有非维度列且为数字列的字段为汇总数据列。

以 order by 的列为准,作为维度列。

其他的列按照插入的顺序保留最开始插入的一行

聚合只会聚合同一个分区内的数据。

只有在同一个批次插入或者分片合并的时候才会进行聚合。如果要是获取汇总值,还是需要使用 sum 进行聚合。

5. SQL

SQL语法

5.1. Insert

-- 插入字段
insert into [table_name] values(...),(....);
-- 插入表
insert into [table_name] select a,b,c from [table_name_2];

5.2. Alter

Delete和Update都是Alter的一种。这类被称作可变查询(Mutation 查询)。

Mutation 查询是一种很重的操作(每次修改或者删除都会导致放弃目标数据的原有分区,重建新分区),而且不支持事务。应该尽量做批量变更,不要作频繁的小数据操作。

ALTER TABLE [db].name ADD INDEX name expression TYPE type GRANULARITY value [FIRST|AFTER name] - 向表元数据添加索引描述。

ALTER TABLE [db].name DROP INDEX name - 从表元数据中删除索引描述并从磁盘中删除索引文件。

-- 前两个命令是轻量级的,它们只更改元数据或删除文件。

ALTER TABLE [db.]table MATERIALIZE INDEX name IN PARTITION partition_name - 查询在分区partition_name中重建二级索引name。 操作为mutation.

ALTER TABLE [db].name DELETE WHERE column_name = value;

ALTER TABLE [db].name UPDATE column_name1 = value1 WHERE column_name2 = value2;
alter table tableName add column newcolname String after col1;

alter table tableName modify column colname String;

alter table tableName drop column colname;

实现高性能update或delete的思路:

create table  A
(
  a xxx,
  b xxx,
  c xxx,
  _sign UInt8,
  _version UInt32
)

==> 更新 : 插入一条新的数据, _version + 1 =》 查询的时候加上一个过滤条件, where version最大

==> 删除: _sign, 0表示未删除,1表示已删除, 同时 version + 1 =》 查询的时候加上一个过滤条件, where _sign=0 and version最大

==> 时间久了,数据膨胀 ==》 类似合并机制,怎么把过期数据清除掉

5.3. 查询

  • 支持子查询

  • 支持 CTE(Common Table Expression 公用表表达式 with 子句)

  • 支持各种JOIN,但是JOIN操作无法使用缓存,所以即使是两次相同的JOIN语句,

    ClickHouse 也会视为两条新 SQL(少用)

  • 窗口函数(官方正在测试中...)

  • 不支持自定义函数

  • GROUP BY 操作增加了 with rollup\with cube\with total 用来计算小计和总计。

    with rollup\with cube\with total
    
    维度是a,b
    rollup:上卷  从右至左去掉维度进行小计
          group by a,b 
          group by a 
          空
    cube:多维分析 从右至左去掉维度进行小计,再从左至右去掉维度进行小计
          group by  a,b 
          group by  a
          group by  b 
          空
    total:总计  只计算合计
          group by  a,b 
          空
    
    select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with rollup;
    
    select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with cube;
    
    select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with totals;
    

5.4. 函数

函数

条件函数

5.5. 导出数据

clickhouse-client --query "select * from t_order_mt where
create_time='2020-06-01 12:00:00'" --format CSVWithNames>
/opt/module/data/rs1.csv

更多格式参照:输入/输出格式

6. EXPLAIN

仅支持20.6以上版本。

  1. 查看 PLAIN(默认)

    explain plan select arrayJoin([1,2,3,null,null]);
    
    explain select database,table,count(1) cnt from system.parts where database in ('datasets','system') group by database,table order by database,cnt desc limit 2 by database;
    
    -- 打开全部的参数的执行计划
    EXPLAIN header=1, actions=1,description=1 SELECT number from
    system.numbers limit 10;
    
  2. AST树

    EXPLAIN AST SELECT number from system.numbers limit 10;
    
  3. SYNTAX语法优化(常用)

    -- 开启三元运算符优化
    SET optimize_if_chain_to_multiif = 1;
    
    -- EXPLAIN SYNTAX + SQL
    EXPLAIN SYNTAX SELECT number = 1 ? 'hello' : (number = 2 ? 'world' : '2021') FROM numbers(10);
    //返回优化后的语句
    SELECT multiIf(number = 1, \'hello\', number = 2, \'world\', \'2021\') FROM numbers(10)
    
  4. 查看PIPELINE(线程情况)

    ```sql EXPLAIN PIPELINE SELECT sum(number) FROM numbers_mt(100000) GROUP BY number % 20;

-- 打开其他参数 EXPLAIN PIPELINE header=1,graph=1 SELECT sum(number) FROM numbers_mt(10000) GROUP BY number%20;


### 老版本查看执行计划

```sql
clickhouse-client -h 主机名 --send_logs_level=trace <<< "SQL语句" > /dev/null

其中,send_logs_level 参数指定日志等级为 trace,<<<将 SQL 语句重定向至 clickhouse-client 进行查询,> /dev/null 将查询结果重定向到空设备吞掉,以便观察日志。

7. 副本

副本的目的主要是保障数据的高可用性,即使一台 ClickHouse 节点宕机,那么也可以从 其他服务器获得相同的数据。

副本只支持合并树家族引擎,且只针对表。

create table t_order_rep2 (
   id UInt32,
sku_id String,
total_amount Decimal(16,2), create_time Datetime
 ) engine =ReplicatedMergeTree('/clickhouse/table/01/t_order_rep','rep_102')
  partition by toYYYYMMDD(create_time)
  primary key (id)
  order by (id,sku_id);

CK采用的是多主模式,即没有主从之分。随意的一个节点修改另一个节点也会进行同步。

7.1. 步骤

  1. 启动ZooKeeper服务

  2. 在CK中添加ZK的配置

    有两种方式:

    1. 在原有的配置文件中进行配置

      config.xml

      <?xml version="1.0"?>
      <yandex>
          <zookeeper-servers>
             <node index="1">
      <host>hadoop102</host>
                 <port>2181</port>
             </node>
             <node index="2">
                 <host>hadoop103</host>
                 <port>2181</port>
             </node>
             <node index="3">
                 <host>hadoop104</host>
                 <port>2181</port>
             </node>
          </zookeeper-servers>
      </yandex>
      
    2. 在外部配置文件中配置后在原有的配置中进行导入

      config.d/metrika.xml:

      <?xml version="1.0"?>
      <yandex>
          <zookeeper-servers>
             <node index="1">
      <host>hadoop102</host>
                 <port>2181</port>
             </node>
             <node index="2">
                 <host>hadoop103</host>
                 <port>2181</port>
             </node>
             <node index="3">
                 <host>hadoop104</host>
                 <port>2181</port>
             </node>
          </zookeeper-servers>
      </yandex>
      

      config.xml

      <include_from>/etc/clickhouse-server/config.d/metrika.xml</include_from>
      
      <!-- 外部导入需要添加下面的属性 -->
      <zookeeper incl="zookeeper-servers" optional="true" />
      
    3. 将上述修改同步到多个主机上,并重启ZK和CK。

    4. 副本只同步表内容,表需要进行分别创建。

      create table t_order_rep2 (
         id UInt32,
      sku_id String,
      total_amount Decimal(16,2), create_time Datetime
       ) engine =ReplicatedMergeTree('/clickhouse/table/01/t_order_rep','rep_102')
        partition by toYYYYMMDD(create_time)
        primary key (id)
        order by (id,sku_id);
      

      其中,ReplicatedMergeTree('/clickhouse/table/01/t_order_rep','rep_102')

      第一个参数是分片的zk_path一般按照:/clickhouse/table/{shard}/{table_name} 的格式写,如果只有一个分片就写 01 即可。

      第二个参数是副本名称,相同的分片副本名称不能相同,所以在不同的节点上都应该不同

8. 分片集群

副本虽然能够提高数据的可用性,降低丢失风险,但是每台服务器实际上必须容纳全量数据,对数据的横向扩容没有解决。

要解决数据水平切分的问题,需要引入分片的概念。通过分片把一份完整的数据进行切分,不同的分片分布到不同的节点上,再通过 Distributed 表引擎把数据拼接起来一同使用。

Distributed 表引擎本身不存储数据,通过分布式逻辑表来写入、分发、路由来操作多台节点不同分片的分布式数据。

ClickHouse 的集群是表级别的,实际企业中,大部分做了高可用,但是没有用分片,避免降低查询性能以及操作集群的复杂性。

8.1. 集群写入和读取流程流程(3分片2副本为例)

image-20211212161735369
Image
image-20211212161746975
Image

8.2. 配置步骤

有两种方式,内部配置和上面副本的内部配置相似。

  1. config.d/metrika.xml

    <remote_servers> 
      <!-- 集群名称,可以自己设置 -->  
      <gmall_cluster> 
        <!--集群的第一个分片-->  
        <shard> 
          <internal_replication>true</internal_replication>
          <!--该分片的第一个副本-->  
          <replica> 
            <host>hadoop102</host>  
            <port>9000</port> 
          </replica>
          <!--该分片的第二个副本-->  
          <replica> 
            <host>hadoop103</host>  
            <port>9000</port> 
          </replica> 
        </shard>
        <!--集群的第二个分片-->  
        <shard> 
          <internal_replication>true</internal_replication>  
          <!--该分片的第一个副本-->
          <replica> 
            <host>hadoop104</host>  
            <port>9000</port> 
          </replica> 
        </shard> 
      </gmall_cluster> 
    </remote_servers>
    
    <zookeeper-servers> 
      <node index="1"> 
        <host>hadoop102</host>  
        <port>2181</port> 
      </node>  
      <node index="2"> 
        <host>hadoop103</host>  
        <port>2181</port> 
      </node>  
      <node index="3"> 
        <host>hadoop104</host>  
        <port>2181</port> 
      </node> 
    </zookeeper-servers>
    
    <!-- 宏指令 -->
    <macros> 
      <!-- 存放的分片数 不同机器放的分片数不一样-->
      <shard>01</shard>
      <!--不同机器放的副本数不一样--> 
      <!-- rep_1_1 副本名 = 名字_分片数_副本数 -->
      <replica>rep_1_1</replica>  
    </macros>
    
  2. config.xml

    <include_from>/etc/clickhouse-server/config.d/metrika.xml</include_from>
    
    <!-- 外部导入需要添加下面的属性 -->
    <zookeeper incl="zookeeper-servers" optional="true" />
    
  3. 同步上述两个文件到其他机器并修改macros中的参数,然后重启全部CK。

  4. 在集群上创建本地表

    create table st_order_mt on cluster gmall_cluster (    -- gmall_cluster为配置文件中指定的集群名字
      id UInt32,
        sku_id String,
        total_amount Decimal(16,2), create_time Datetime
     ) engine
    =ReplicatedMergeTree('/clickhouse/tables/{shard}/st_order_mt','{replica}')    -- 此处引擎要用分布式的合并树家族的引擎之一,(zk_path){shard}和(副本名){replica}会自动读取<macros>中的变量。st_order_mt此处和创建的表名一致
      partition by toYYYYMMDD(create_time)
      primary key (id)
      order by (id,sku_id);
    
  5. 创建分布式表

    create table st_order_mt_all2 on cluster gmall_cluster    -- -- gmall_cluster为配置文件中指定的集群名字
    (
        id UInt32, 
      sku_id String,
        total_amount Decimal(16,2), create_time Datetime
    )engine = Distributed(gmall_cluster,default, st_order_mt,hiveHash(sku_id));
    

    Distributed(集群名称,库名,本地表名,分片键);

  6. 使用分布式表进行数据操作

    insert into st_order_mt_all2 values
    (201,'sku_001',1000.00,'2020-06-01 12:00:00') ,
    (202,'sku_002',2000.00,'2020-06-01 12:00:00'),
    (203,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (204,'sku_002',2000.00,'2020-06-01 12:00:00'),
    (205,'sku_003',600.00,'2020-06-02 12:00:00');
    
  7. 查询的时候使用分布式表

    SELECT * FROM st_order_mt_all;
    

    注意:

    查询本地表只会查询到本机存储部分的信息。

    查询到的结果会分区、分片显示(clickhouse-client)。

    相同分片下的不同副本存储的信息相同。

    不同分片存储的信息不同。

9. 优化

9.1. 表优化

  1. 时间字段的类型

    建表时能用数值型或日期时间型表示的字段就不要用字符串。

    ClickHouse 底层将 DateTime 存储为时间戳 Long 类型,但不建议存储 Long 类型, 因为 DateTime 不需要经过函数转换处理,执行效率高、可读性好。

  2. 空值存储类型

    Nullable类型几乎总是会拖累性能,因为存储Nullable列时需要创建一个 额外的文件(column_name.null.bin)来存储 NULL 的标记,并且 Nullable 列无法被索引。

    除非极特殊情况,应直 接使用字段默认值表示空,或者自行指定一个在业务中无意义的值(例如用-1 表示没有商品 ID)。

  3. 分区和索引

    一般选择按天分区,也可以指定为 Tuple(), 以单表一亿数据为例,分区大小控制在 10-30 个为最佳。

    必须指定索引列,ClickHouse 中的索引列即排序列,通过 order by 指定。

    通常需要满足高级列在前、查询频率大的在前原则。

    基数特别大的不适合做索引列, 如用户表的 userid 字段;通常筛选后的数据满足在百万以内为最佳。

    PARTITION BY toYYYYMM(EventDate)
    ORDER BY (CounterID, EventDate, intHash32(UserID))
    
    PARTITION BY toYYYYMM(StartDate)
    ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID)
    
  4. 表参数

    Index_granularity 是用来控制索引粒度的,默认是 8192,如非必须不建议调整。

    如果表中不是必须保留全量历史数据,建议指定 TTL(生存时间值),可以免去手动过期历史数据的麻烦,TTL 也可以通过 alter table 语句随时修改。

  5. 写入和删除优化

    1. 尽量不要执行单条或小批量删除和插入操作,这样会产生小分区文件,给后台 Merge 任务带来巨大压力。

    2. 不要一次写入太多分区,或数据写入太快,数据写入太快会导致 Merge 速度跟不上而报错,一般建议每秒钟发起 2-3 次写入操作,每次操作写入 2w~5w 条数据(依服务器性能而定

      写入过快报错,报错信息:

      1. Code: 252, e.displayText() = DB::Exception: Too many parts(304).
      Merges are processing significantly slower than inserts
      2. Code: 241, e.displayText() = DB::Exception: Memory limit (for query)
      exceeded:would use 9.37 GiB (attempt to allocate chunk of 301989888
      bytes), maximum: 9.31 GiB
      

      处理方式: “ Too many parts 处理 ” :使用 WAL 预写日志,提高写入性能。in_memory_parts_enable_wal 默认为 true

      在服务器内存充裕的情况下增加内存配额,一般通过 max_memory_usage 来实现在服务器内存不充裕的情况下,建议将超出部分内容分配到系统硬盘上,但会降低执行速度,一般通过 max_bytes_before_external_group_by、max_bytes_before_external_sort 参数来实现。

9.2. 配置优化

配置项主要在 config.xml 或 users.xml 中, 基本上都在 users.xml 里。

config.xml的配置项

users.xml的配置项

CPU资源

配置 描述
background_pool_size 后台线程池的大小,merge 线程就是在该线程池中执行,该线程池不仅仅是给 merge 线程用的,默认值 16,允许的前提下建议改成 c pu 个数的 2 倍(线程数)。
background_schedule_pool_size 执行后台任务(复制表、Kafka 流、DNS 缓存更新)的线程数。默 认 128,建议改成 cpu 个数的 2 倍(线程数)。
backgrounddistributed_schedule pool_size 设置为分布式发送执行后台任务的线程数,默认 16,建议改成 cpu 个数的 2 倍(线程数)。
max_concurrent_queries 最大并发处理的请求数(包含 select,insert 等),默认值 100,推荐 150(不够再加)~300。
max_threads 设置单个查询所能使用的最大 cpu 个数,默认是 cpu 核数。

内存资源

配置 描述
max_memory_usage 此参数在 users.xml 中,表示单次 Query 占用内存最大值,该值可 以设置的比较大,这样可以提升集群查询的上限。 保留一点给 OS,比如 128G 内存的机器,设置为 100GB。
maxbytes_before_external_group by 一般按照 max_memory_usage 的一半设置内存,当 group 使用内 存超过阈值后会刷新到磁盘进行。 因为 clickhouse 聚合分两个阶段:查询并及建立中间数据、合并中间数据,结合上一项,建议 50GB。
max_bytes_before_external_sort 当 order by 已使用 max_bytes_before_external_sort 内存就进行 溢写磁盘(基于磁盘排序),如果不设置该值,那么当内存不够时直接 抛错,设置了该值 order by 可以正常完成,但是速度相对存内存来 说肯定要慢点(实测慢的非常多,无法接受)。
max_table_size_to_drop 此参数在 config.xml 中,应用于需要删除表或分区的情况,默认是 50GB,意思是如果删除 50GB 以上的分区表会失败。建议修改为 0, 这样不管多大的分区表都可以删除。

存储

ClickHouse 不支持设置多数据目录,为了提升数据 IO 性能,可以挂载虚拟卷组,一个卷组绑定多块物理磁盘提升读写性能。

9.3. 语法优化

  1. count函数优化

    如果使用的是 count() 或者 count(*),且没有 where 条件,则会直接使用 system.tables 的 total_rows(counts.txt)

    如果 count 具体的列字段,则不会使用此项优化。

  2. 消除子查询重复字段

    表的字段相同,比如select a, a会被优化成select a

    也会删除重复的order by key,limit by key, using key,只要判断是同一个表中的同一个字段就会被优化,即使用了别名也没用。

  3. 谓词下推

    当 group by 有 having 子句,但是没有 with cube、with rollup 或者 with totals 修饰的时候,having 过滤会下推到 where 提前过滤。例如下面的查询,HAVING name 变成了WHERE name,在 group by 之前过滤。子查询也支持谓词下推。

    EXPLAIN SYNTAX 
        SELECT UserID 
        FROM hits_v1 
        GROUP BY UserID 
        HAVING UserID = '8585742290196126178';
    
    -- 返回优化语句
    SELECT UserID
    FROM hits_v1
    WHERE UserID = \'8585742290196126178\' GROUP BY UserID
    
    EXPLAIN SYNTAX SELECT
    * 
    FROM
        ( SELECT * FROM ( SELECT UserID FROM visits_v1 ) UNION ALL SELECT * FROM ( SELECT UserID FROM visits_v1 ) ) 
    WHERE
        UserID = '8585742290196126178'
    
    -- 返回优化语句
    SELECT
        UserID 
    FROM
        (
        SELECT
            UserID 
        FROM
            (
            SELECT
                UserID 
            FROM
                visits_v1 
            WHERE
                UserID = \ '8585742290196126178\'
                )
                WHERE UserID = \'8585742290196126178\'
                UNION ALL
                SELECT UserID
                FROM
                (
                SELECT UserID
                FROM visits_v1
                WHERE UserID = \'8585742290196126178\'
                )
                WHERE UserID = \'8585742290196126178\'
            )
        WHERE UserID = \'8585742290196126178\'
    
  4. 聚合计算外推

    sum(a * 2)会被优化成sum(a) * 2

  5. 聚合函数消除

    EXPLAIN SYNTAX
    SELECT
       sum(UserID * 2),
       max(VisitID),
       max(UserID)
    FROM visits_v1
    GROUP BY UserID
    
    -- 返回优化语句
    SELECT
       sum(UserID) * 2,
       max(VisitID),
         UserID    -- 此时max(UserID)无意义,被优化
    FROM visits_v1
    GROUP BY UserID
    
  6. 标量替换

    如果子查询只返回一行数据,在被引用的时候用标量替换。

  7. 三元运算优化

    如果开启了 optimize_if_chain_to_multiif 参数,三元运算符会被替换成 multiIf 函数。

9.4. 查询优化

单表查询

  1. PREWHERE替代WHERE

    Prewhere 和 where 语句的作用相同,用来过滤数据。不同之处在于 prewhere 只支持 *MergeTree 族系列引擎的表。

    Prewhere会读取指定的列数据,按照条件进行过滤后再读取select需要的列字段进行其他属性的补全。

    当查询列明显多于筛选列时使用 Prewhere可十倍提升查询性能,Prewhere 会自动优化执行过滤阶段的数据读取方式,降低 io 操作。

    在某些场合下,prewhere 语句比 where 语句处理的数据量更少性能更高。

    -- 开关where自动转where(默认开)
    set optimize_move_to_prewhere=0;    -- 关
    set optimize_move_to_prewhere=0;    -- 开
    

    默认情况,我们不会关闭 where 自动优化成 prewhere。

    但是某些场景即使开启优化,也不会自动转换成 prewhere,需要手动指定 prewhere(即把where关键字换成prewhere即可)。

    不会自动转换成prewhere的情况:

    1. 使用常量表达式
    2. 默认值为alias类型的字段
    3. 包含了arrayJOIN、globalIn、globalNotIn或者indexHint的查询
    4. select查询的列字段和where的谓词相同
    5. 使用了主键字段
  2. 数据采样

    SELECT Title,count(*) AS PageViews
    FROM hits_v1
    SAMPLE 0.1 #代表采样 10%的数据,也可以是具体的条数 WHERE CounterID =57
    GROUP BY Title
    ORDER BY PageViews DESC LIMIT 1000
    

    采样修饰符只有在 MergeTree engine 表中才有效,且在创建表时需要指定采样策略。

  3. 列裁剪和分区裁剪

    1. 数据量太大时应避免使用 select * 操作(宽表),字段越少,消耗的 io 资源越少,性能就会越高。
    2. 分区裁剪就是只读取需要的分区,在过滤条件中指定。where EventDate='2014-03-23';
  4. orderby 结合 where、limit

    避免全表排序,可能会导致资源不足或者检索过长。千万以上数据集进行 order by 查询时需要搭配 where 条件和 limit 语句一起使用。

    SELECT UserID,Age
    FROM hits_v1
    ORDER BY Age DESC LIMIT 1000
    
  5. 避免构建虚列,可以在前端进行处理或者在表中构造实际字段进行额外存储。

  6. uniqCombined 替代 distinct

    性能可提升 10 倍以上,uniqCombined 底层采用类似 HyperLogLog 算法实现,能接收 2% 左右的数据误差(对误差要求比较严格的还是用distinct),可直接使用这种去重方式提升查询性能。Count(distinct)会使用 uniqExact 精确去重。

    不建议在千万级不同数据上执行 distinct 去重查询,改为近似去重 uniqCombined。

  7. 使用物化视图

  8. 其他注意事项

    1. 查询熔断 为了避免因个别慢查询引起的服务雪崩的问题,除了可以为单个查询设置超时以外,还 可以配置周期熔断,在一个查询周期内,如果用户频繁进行慢查询操作超出规定阈值后将无 法继续进行查询操作。

    2. 关闭虚拟内存

      物理内存和虚拟内存的数据交换,会导致查询变慢,资源允许的情况下关闭虚拟内存。

    3. 配置 join_use_nulls 为每一个账户添加 join_use_nulls 配置,左表中的一条记录在右表中不存在,右表的相 应字段会返回该字段相应数据类型的默认值,而不是标准 SQL 中的 Null 值。

    4. 批量写入时先排序

      批量写入数据时,必须控制每个批次的数据中涉及到的分区的数量,在写入之前最好对 需要导入的数据进行排序。无序的数据或者涉及的分区太多,会导致 ClickHouse 无法及时对 新导入的数据进行合并,从而影响查询性能。

    5. 关注 CPU cpu 一般在 50%左右会出现查询波动,达到 70%会出现大范围的查询超时,cpu 是最关 键的指标,要非常关注。

多表关联

多表关联应该尽量减少使用join,CK的join原理:将右表加载到内存,再和左表进行匹配。

  1. 如果语句能够使用in替换,则使用in。

  2. 满足小表在右的原则(这里和MySQL相反),减小内存的使用。

  3. 限制条件尽量往上提,能先过滤先过滤,减小数据的比较。(谓词下推)

    这里有个小点,having是根据结果进行过滤,可以考虑能否用where替代(先过滤再进行比较),进而可以再考虑是否可以使用prewhere(读取指定的列数据,按照条件进行过滤后再读取select需要的列字段进行其他属性的补全)。

  4. 使用字典表

  5. 分布式表中要使用GLOBAL关键字

    两张分布式表上的 IN 和 JOIN 之前必须加上 GLOBAL 关键字,右表只会在接收查询请求的那个节点查询一次,并将其分发到其他节点上。如果不加 GLOBAL 关键字的话,每个节点都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询 N2(N 是该分布式表的分片数量),这就是查询放大,会带来很大开销。

10. 数据一致性

注意点:ReplacingMergeTree不能保证数据一定没有重复(未merge等情况),只能保证数据的最终一致性

保持数据一致性的解决方案:

  1. 手动去重

    OPTIMIZE TABLE table_name PARTITION partition_name FINAL;
    

    生产环境不推荐。

  2. 使用SQL实现去重(加标记字段)

    -- create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据; deleted 是自 定的一个标记位,比如 0 代表未删除,1 代表删除数据。
    
    SELECT
        user_id ,
      argMax(score, create_time) AS score,
      argMax(deleted, create_time) AS deleted,
      max(create_time) AS ctime
    FROM test_a
    GROUP BY user_id
    HAVING deleted = 0;
    
    -- argMax(field1,field2):按照 field2 的最大值取 field1 的值。
    -- 当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的create_time 得到修改后的 score 字段值。
    
    CREATE VIEW view_test_a AS
    SELECT
      user_id ,
      argMax(score, create_time) AS score,
      argMax(deleted, create_time) AS deleted,
      max(create_time) AS ctime
    FROM test_a
    GROUP BY user_id
    HAVING deleted = 0;
    
    SELECT *
    FROM view_test_a WHERE user_id = 0;
    
    -- 透过创建视图,可以将筛选的操作保存起来。
    

    在这种情况下,进行删除或者更新,本质上还是进行数据的插入。最终透过条件筛选得出最新的未被标记为删除的数据。

    数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合表级别的 TTL 最终将物理数据删除。

  3. 使用FINAL。

    老版本的FINAL是单线程。

    新版本的FINAL是多线程,但是读取part的过程是串行的。

    SELECT * FROM visits_v1 FINAL WHERE StartDate = '2014-03-17' LIMIT 100
    
  4. 对数据重复要求比较低,并且数据误差不大,不管。

11. 物化视图

物化视图和普通视图的区别:普通视图不保存数据,保存的仅仅是查询语句。物化视图则是把查询的结果根据相应的引擎存入到了磁盘 或内存中。

优点:查询速度快,要是把物化视图这些规则全部写好,它比原数据查询快了很多,总的行数少了,因为都预计算好了。

缺点:1. 它的本质是一个流式数据的使用场景,是累加式的技术,所以要用历史数据做去重、去核这样的分析,在物化视图里面是不太好用的。在某些场景的使用也是有限的。而且 如果一张表加了好多物化视图,在写这张表的时候,就会消耗很多机器的资源,比如数据带宽占满、存储一下子增加了很多。2. 物化视图的 alter 操作有些限制,操作起来不大方便。3. 物化视图的 alter 操作有些限制,操作起来不大方便。物化视图不支持同步删除,若源表的数据不存在(删除了)则物化视图的数据仍然保留。

11.1. 语法

CREATE [MATERIALIZED] VIEW [IF NOT EXISTS]    -- 视图名
[db.]table_name    -- 原表
-- 1. 默认创建一个隐藏的目标表( .inner.物化视图名)来保存视图数据。使用 TO 表名,保存到一张显式的表。
-- 2. 若物化视图的定义使用了 TO [db.]name 子语句,则可以将目标表的视图卸载DETACH 再装载 ATTACH
[TO[db.]name]    
[ENGINE = engine]    -- 必须指定物化视图的 engine 用于数据存储
[POPULATE]    -- 是否带入历史数据(不推荐),需要历史数据自己进行insert即可。且TO [db].[table]语法的时候,不得使用 POPULATE。
    AS 
    SELECT ...



#建表语句
CREATE MATERIALIZED VIEW hits_mv
ENGINE=SummingMergeTree
PARTITION BY toYYYYMM(EventDate) 
ORDER BY (EventDate, intHash32(UserID)) 
AS 
    SELECT
    UserID,
    EventDate,
    count(URL) as ClickCount,
    sum(Income) AS IncomeSum
  FROM hits_test
  WHERE EventDate >= '2014-03-20' #设置更新点,该时间点之前的数据可以另外通过
  GROUP BY UserID,EventDate;

-- 导入增量数据只需要在原表进行插入即可。

-- 导入历史数据
INSERT INTO view_name 
SELECT
    ...

物化视图创建好之后,若源表被写入新数据则物化视图也会同步更新。物化视图不支持同步删除,若源表的数据不存在(删除了)则物化视图的数据仍然保留。

POPULATE

若有 POPULATE 则在创建视图的过程会将源表已经存在的数据一并导入。若无 POPULATE 则物化视图在创建之后没有数据,只会在创建只有同步之后写入源表的数据

官方并不推荐使用 POPULATE,因为在创建物化视图的过程中同时写入的数据不能被插入物化视图。

物化视图是一种特殊的数据表,可以用 show tables 查看。

12. 参数聚合函数

12.1. 路径匹配

  • sequenceMatch(pattern)(timestamp, cond1, cond2)匹配1,不匹配0
  • sequenceCount(pattern)(timestamp, cond1, cond2)计数

用法

-- 建表语句
create  table action_log(
    id   UInt16,
    ref_date  DateTime,
    action  String
)engine=MergeTree()
partition by ref_date
order by ref_date;


-- 插入数据
insert into action_log 
values (1,toDateTime('2021-03-19 14:00:00'),'点赞'),
(1,toDateTime('2021-03-19 14:01:00'),'投币'),
(1,toDateTime('2021-03-19 14:02:00'),'分享'),
(1,toDateTime('2021-03-19 14:03:00'),'转发'),
(1,toDateTime('2021-03-19 14:04:00'),'一键三连');


-- 2在1的后面紧挨着
SELECT sequenceMatch('(?1)(?2)')(ref_date , action = '点赞', action = '投币', action = '分享', action = '转发', action = '一键三连') as cnt  FROM action_log 

-- 2在1的后面紧挨着
-- 1和2中间的时间<=60s
SELECT sequenceMatch('(?1)(?t<=60)(?2)')(ref_date , action = '点赞', action = '投币', action = '分享', action = '转发', action = '一键三连') as cnt  FROM action_log 

-- 4在1的后面 中间隔着两个 两个.
SELECT sequenceMatch('(?1)..(?4)')(ref_date, action = '点赞', action = '投币', action = '分享', action = '转发', action = '一键三连')   FROM action_log 

-- 5在1的后面任意个 用.*表示
SELECT sequenceMatch('(?1).*(?5)')(ref_date, action = '点赞', action = '投币', action = '分享', action = '转发', action = '一键三连')   FROM action_log

12.2. windowFunnel

搜索滑动时间窗中的事件链,并计算从链中发生的最大事件数。

该函数采用如下算法:

  • 该函数搜索触发链中的第一个条件并将事件计数器设置为1。 这是滑动窗口启动的时刻。
  • 如果来自链的事件在窗口内顺序发生,则计数器将递增。 如果事件序列中断,则计数器不再增加。

  • 如果数据在不同的完成点具有多个事件链,则该函数将仅输出最长链的大小。

语法

windowFunnel(window, [mode, [mode, ... ]])(timestamp, cond1, cond2, ..., condN)
  • window — 滑动窗户的大小,表示事件链中第一个事件和最后一个事件的最大间隔。 单位取决于timestamp。用表达式来表示则是:timestamp of cond1 <= timestamp of cond2 <= ... <= timestamp of condN <= timestamp of cond1 + window。
  • mode - 这是一个可选的参数,可以设置一个或多个参数。
    • 'strict_deduplication' - 如果事件链中出现相同的条件,则会停止进一步搜索。
    • 'strict_order' - 不允许其他事件的介入。 例如:在A->B->D->C的情况下,它在D停止继续搜索A->B->C,最大事件数为2。
    • 'strict_increase' - 事件链中的时间戳必须严格上升。
  • timestamp — 包含时间戳的列。 数据类型支持: 日期, 日期时间 和其他无符号整数类型(请注意,即使时间戳支持 UInt64 类型,它的值也不能超过Int64最大值,即2^63-1)。
  • cond — 事件链的约束条件。 UInt8 类型。
SELECT 
  level,user_count,conv_rate_percent
FROM (
  SELECT 
    level,
    uniqCombined(user_id) AS user_count,
    neighbor(user_count, -1) AS prev_user_count,
    if (prev_user_count = 0, -1, round(user_count / prev_user_count * 100, 3)) AS conv_rate_percent
  FROM (
    SELECT
      user_id,
      windowFunnel(900)(
        ts_date_time,
        event_type = 'shtLogon',
        event_type = 'shtKkClick' AND column_type = 'homePage',
        event_type = 'shtOpenGoodsDetail',
        event_type = 'shtAddCart',
        event_type = 'shtOrderDone'
      ) AS level
    FROM (
      SELECT ts_date,ts_date_time,event_type,column_type,user_id
      FROM ods.analytics_access_log_all
      WHERE ts_date >= '2020-06-30' AND ts_date <= '2020-07-02'
      AND site_id IN (10266,10022,10339,10030)
    )
    GROUP BY user_id
  )
  WHERE level > 0
  GROUP BY level
  ORDER BY level ASC
);

漏斗分析


CREATE TABLE action
(
    `uid` Int32,
    `event_type` String,
    `time` datetime
)
ENGINE = MergeTree()
PARTITION BY uid
ORDER BY xxHash32(uid)
SAMPLE BY xxHash32(uid)
SETTINGS index_granularity = 8192


insert into action values(1,'浏览','2020-01-02 11:00:00');
insert into action values(1,'点击','2020-01-02 11:10:00');
insert into action values(1,'下单','2020-01-02 11:20:00');
insert into action values(1,'支付','2020-01-02 11:30:00');

insert into action values(2,'下单','2020-01-02 11:00:00');
insert into action values(2,'支付','2020-01-02 11:10:00');

insert into action values(1,'浏览','2020-01-02 11:00:00');

insert into action values(3,'浏览','2020-01-02 11:20:00');
insert into action values(3,'点击','2020-01-02 12:00:00');

insert into action values(4,'浏览','2020-01-02 11:50:00');
insert into action values(4,'点击','2020-01-02 12:00:00');

insert into action values(5,'浏览','2020-01-02 11:50:00');
insert into action values(5,'点击','2020-01-02 12:00:00');
insert into action values(5,'下单','2020-01-02 11:10:00');

insert into action values(6,'浏览','2020-01-02 11:50:00');
insert into action values(6,'点击','2020-01-02 12:00:00');
insert into action values(6,'下单','2020-01-02 12:10:00');






    SELECT 
        user_id,
        windowFunnel(1800)(time, event_type = '浏览', event_type = '点击', event_type = '下单', event_type = '支付') AS level
    FROM 
    (
        SELECT 
            time,
            event_type,
            uid AS user_id
        FROM action
    )
    GROUP BY user_id


    SELECT
        arrayJoin(range(5)) AS level,    -- range(5) = [0,1,2,3,4]
        arraySort(    -- 升序排序
            arrayMap(    -- 将列上的数据转换成数组
                x -> x,
                groupArray(lv)
            )
        ) as arr,    -- 将列转换成数组并升序排序,方便下一层计算
        arrayFilter(    -- 过滤函数
            (x,y) -> x >= y,    -- 筛选条件,如果数字比level大或者等,则表示经过这一个level
            arr,    -- 上面整理好的数组
            arrayWithConstant(    -- level填充,方便上层函数计算。会生成成内容为level,长度为arr的数组。
                LENGTH (arr),
                level
            )
        ) as arr2,    -- 生成了一个通过这一level的数组
        length(arr2) as count    -- 对arr2计数,计算通过这一层的个数(最终结果)。
    FROM
    (
        SELECT 
            user_id,
            windowFunnel(1800)(time, event_type = '浏览', event_type = '点击', event_type = '下单', event_type = '支付') AS lv
        FROM 
        (
            SELECT 
                time,
                event_type,
                uid AS user_id
            FROM action
        )
        GROUP BY user_id
    )

13. 其他

  1. 使用with totals语句后,sum统计行在DataGrip中并没有给出,但在clickhouse-client和DBeaver中都正常显示。应该是DataGrip的锅。

    企业微信截图_16391065389552
    Image
    企业微信截图_1639107407230
    Image
  2. 在查询指定日期的情况下,如果某个日期下没有数据,则这个日期不会显示,解决办法:

    思路:生成一个包含全部时间的表,列如果没有则指定为默认值,然后和原来的表做全连接,最后使用聚合函数配合group by合并即可。

    SELECT t, SUM(metric) as metric FROM (
        SELECT 
            arrayJoin(
              arrayMap( x -> toStartOfHour(addHours(toDateTime($from),x)),
                  range(toUInt64(
                      dateDiff('hour', 
                          toDateTime($from), 
                          toDateTime($to)) + 1)))
            ) as t,
            0 as metric
    
        UNION ALL
    
        SELECT
            toStartOfHour(my_date) as t,
            COUNT(metric)
            FROM my_table
            WHERE t BETWEEN toDateTime($from) AND toDateTime($to)
            GROUP BY t
    )
    GROUP BY t ORDER BY t
    

    https://stackoom.com/question/3OnLU

  3. 跨大版本升级的时候,最好使用SELECT FROM remote的方法。像从20.9.6.14升级到22.2.2.1,bitmap的位数原版本为32,后者为64,会报错Code: 1001. DB::Exception: Received from 192.168.1.1:9000 DB::Exception: std::runtime_error: failed alloc while reading. (STD_EXCEPTION)(vesrion 22.2.2.1)

    Issue

Copyright © rootwhois.cn 2021-2022 all right reserved,powered by GitbookFile Modify: 2023-03-05 10:55:52

results matching ""

    No results matching ""